/*
 ------------------------------------------------------------------------------
 Copyright (C) 2013 Eternal Games.

 This file is part of the EternalTech source code.

 The EternalTech source code is free software; you can redistribute it and/or
 modify it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 2 of the License, or (at your
 option) any later version.

 The EternalTech source code is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 more details.

 You should have received a copy of the GNU General Public License along with
 the EternalTech source code; if not, write to the Free Software Foundation, Inc.,
 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 ------------------------------------------------------------------------------
*/


//
// cl_server.cpp - server interaction
//


#include "client.h"


/*
 ==================
 
 Called when a connection, demo, or cinematic is being terminated.
 Sends a disconnect message to the server and restarts all the subsystems.
 This is also called when an error is thrown, so it shouldn't cause any errors.
 ==================
*/
void CL_Disconnect (bool restart){

	// If playing a demo, get statistics

	// Send a disconnect message to the server (send it a few times in case one
	// is dropped)
	if (cls.state >= CS_CONNECTED && !clc.demoPlaying){
		CL_AddReliableCommand("disconnect");

		CL_SendPacket();
		CL_SendPacket();
		CL_SendPacket();
	}

	// Shutdown game module
	CG_Shutdown();

	// Wipe the entire clientActive_t structure
	Mem_Fill(&cl, 0, sizeof(clientActive_t));

	// Stop download

	// Stop demo recording

	// Stop demo playback

	// Wipe the entire clientConnection_t structure
	Mem_Fill(&clc, 0, sizeof(clientConnection_t));

	// Stop cinematic playback

	// Clear key and input states
	Key_ClearStates();
	IN_ClearStates();

	// Make sure sounds aren't playing
	S_StopAllSounds();

	// Stop capturing voice
	S_StopVoiceCapture(true);

	// Force main menu GUI and console off
	GUI_Close();
	Con_Close();

	// If a restart is desired
	if (restart && cls.state >= CS_CONNECTED){
		// Shutdown all the subsystems
		CL_ShutdownAll();

		// Free the clip map
		CM_FreeMap();

		// Don't sync variables
		CVar_SyncVariables(NULL);

		// Allow cheats
		CVar_FixCheatVariables(true);

		// Initialize all the subsystems
		CL_InitAll();
	}

	// We are now fully disconnected from the server
	cls.state = CS_DISCONNECTED;

	// Clear variables

	// Reset the com_timeDemo variable

	// If time demo or AVI demo was active
}

/*
 ==================
 CL_Drop

 Called after a non-fatal error was thrown
 ==================
*/
void CL_Drop (const char *message){

	if (cls.state == CS_UNINITIALIZED)
		return;		// Nothing running on the client

	// If connected to a server, disconnect
	CL_Disconnect(true);

	// TODO: GUI stuff goes here
}

/*
 ==================
 
 ==================
*/
void CL_LocalConnect (){

	if (cls.state == CS_UNINITIALIZED)
		return;		// Nothing running on the client

	// If we're already connected to the local server, stay connected
	if (cls.state >= CS_CONNECTED && NET_IsLocalAddress(clc.netChan.remoteAddress)){
		cls.state = CS_CONNECTED;

		// Free the level
		CG_FreeLevel();

		// Shutdown game module
		CG_Shutdown();

		// TODO: stop demo recording

		// Free the clip map from a previous level
		CM_FreeMap();

		// Update the screen before the local server loads the level
		CL_UpdateScreen();

		return;
	}

	// If connected to a server, disconnect
	CL_Disconnect(false);

	// Connect to the local server
	Str_Copy(cls.serverName, "localhost", sizeof(cls.serverName));

	NET_StringToAddress(cls.serverName, &clc.serverAddress);

	// CL_CheckForResend will fire immediately
	cls.state = CS_CHALLENGING;

	clc.connectTime = cls.frameTime - RESEND_TIMEOUT;
	clc.connectCount = 0;
	clc.connectRejected = false;

	// Update the screen before the local server loads the level
	CL_UpdateScreen();
}

/*
 ==================
 CL_AddReliableCommand

 The given command will be sent to the server, and is guaranteed to not have
 any future user command processed before it is executed
 ==================
*/
void CL_AddReliableCommand (const char *fmt, ...){

	char	command[MAX_STRING_LENGTH];
	va_list	argPtr;
	int		index;
	int		len;

	if (cls.state < CS_CONNECTED || clc.demoPlaying)
		return;

	va_start(argPtr, fmt);
	len = Str_VSPrintf(command, sizeof(command), fmt, argPtr);
	va_end(argPtr);

	if (len == -1)
		Com_Error(false, "CL_AddReliableCommand: command string overflow");

	// If we would be losing an old command that hasn't been acknowledged, we
	// must drop the connection
	if (clc.reliableCommandSequence - clc.reliableCommandAcknowledge == MAX_RELIABLE_COMMANDS)
		Com_Error(false, "CL_AddReliableCommand: command buffer overflow");

	// Store it off
	clc.reliableCommandSequence++;

	index = clc.reliableCommandSequence & (MAX_RELIABLE_COMMANDS - 1);
	Str_Copy(clc.reliableCommandList[index], command, sizeof(clc.reliableCommandList[index]));
}

/*
 ==================
 CL_ForwardCommandToServer

 Adds the current command line as a reliable command.
 Things like "god", "noclip", etc, are commands directed to the server, so when
 they are typed in at the console, they will need to be forwarded.
 ==================
*/
void CL_ForwardCommandToServer (){

	if (cls.state != CS_ACTIVE || clc.demoPlaying){
		Com_Printf("Unknown command \"%s\"\n", Cmd_Argv(0));
		return;
	}

	CL_AddReliableCommand("%s", Cmd_Args(0, -1, false));
}

/*
 ==================
 
 ==================
*/
static void CL_VerifyPureClientAtServer (){

	char	string[MAX_STRING_LENGTH];

	string[0] = 0;

	// Send a pure verification request to the server.
	// If everything is fine, the server will send us the game state so we can
	// finally load the level.
	CL_AddReliableCommand("verifyPure %s", string);
}


/*
 ==============================================================================

 FILE DOWNLOADING

 ==============================================================================
*/


/*
 ==================
 
 ==================
*/
static void CL_DownloadsComplete (){

	// Initialize game module
	CG_Init();

	// Request a pure verification to the server before loading anything at all
	CL_VerifyPureClientAtServer();
}

/*
 ==================
 
 ==================
*/
void CL_RequestNextDownload (){

}

/*
 ==================
 
 ==================
*/
void CL_RequestDownloads (){

	// If we're connected to the local server or playing a demo, we don't need
	// to download anything
	if (NET_IsLocalAddress(clc.netChan.remoteAddress) || clc.demoPlaying){
		CL_DownloadsComplete();
		return;
	}

	// TODO!!!
}


/*
 ==============================================================================

 SERVER PACKET PARSING

 ==============================================================================
*/


/*
 ==================
 CL_ChallengeResponsePacket
 ==================
*/
static void CL_ChallengeResponsePacket (const netAdr_t from){

	if (cls.state != CS_CONNECTING){
		if (cls.state == CS_CHALLENGING)
			Com_Printf("Dup 'challengeResponse' received. Ignored.\n");
		else
			Com_Printf("Unwanted 'challengeResponse' received. Ignored.\n");

		return;
	}

	if (!NET_CompareAddress(from, clc.serverAddress)){
		Com_Printf("'challengeResponse' received from invalid address. Ignored.\n");
		return;
	}

	clc.serverChallenge = Str_ToInteger(Cmd_Argv(1));

	// CL_CheckForResend will fire immediately
	cls.state = CS_CHALLENGING;

	clc.connectTime = cls.frameTime - RESEND_TIMEOUT;
	clc.connectCount = 0;
	clc.connectRejected = false;
}

/*
 ==================
 CL_ConnectResponsePacket
 ==================
*/
static void CL_ConnectResponsePacket (const netAdr_t from){

	const char	*reason;

	if (cls.state != CS_CHALLENGING){
		if (cls.state == CS_CONNECTED)
			Com_Printf("Dup 'connectResponse' received. Ignored.\n");
		else
			Com_Printf("Unwanted 'connectResponse' received. Ignored.\n");

		return;
	}

	if (!NET_CompareAddress(from, clc.serverAddress)){
		Com_Printf("'connectResponse' received from invalid address. Ignored.\n");
		return;
	}

	// Check for a connection rejected response
	if (Cmd_Argc() != 1){
		if (clc.connectRejected)
			return;		// Already rejected
		clc.connectRejected = true;

		reason = Cmd_Args(1, -1, false);

		// Print the connection rejected reason to the console
		Com_Printf("%s\n", reason);

		// TODO: display the reason to the GUI

		return;
	}

	// Set up the network channel
	cls.state = CS_CONNECTED;

	NetChan_Setup(&clc.netChan, NS_CLIENT, from, CVar_GetVariableInteger("net_channelPort"));

	clc.lastSentTime = cls.frameTime - 999999;	// Send a packet immediately
	clc.lastReceivedTime = cls.frameTime;		// Don't time-out

	clc.timeOutCount = 0;
}

/*
 ==================
 CL_PrintPacket
 ==================
*/
static void CL_PrintPacket (const netAdr_t from){

	if (cls.state == CS_DISCONNECTED){
		Com_Printf("'print' received while disconnected. Ignored.\n");
		return;
	}

	if (!NET_CompareAddress(from, clc.serverAddress)){
		Com_Printf("'print' received from invalid address. Ignored.\n");
		return;
	}

	Com_Printf("%s\n", Cmd_Args(1, -1, false));
}

/*
 ==================
 
 ==================
*/
static void CL_RConPrintPacket (const netAdr_t from){

}

/*
 ==================
 
 ==================
*/
static void CL_ConnectionlessPacket (const netAdr_t from, msg_t *msg){

	char		string[MAX_STRING_LENGTH];
	const char	*cmd;

	// Read the message
	MSG_ClearReadState(msg);

	MSG_ReadLong(msg);
	MSG_ReadString(msg, string, sizeof(string));

	// Tokenize the string
	Cmd_TokenizeString(string);

	if (!Cmd_Argc())
		return;		// No tokens

	cmd = Cmd_Argv(0);

	Com_DPrintf("Server out-of-band packet from %s: %s\n", NET_AddressToString(from), cmd);

	// Server responding to a 'getChallenge' command
	if (!Str_ICompare(cmd, "challengeResponse")){
		CL_ChallengeResponsePacket(from);
		return;
	}

	// Server responding to a 'connect' command
	if (!Str_ICompare(cmd, "connectResponse")){
		CL_ConnectResponsePacket(from);
		return;
	}

	// Print command from server
	if (!Str_ICompare(cmd, "print")){
		CL_PrintPacket(from);
		return;
	}

	// Print command from remote console server
	if (!Str_ICompare(cmd, "rconPrint")){
		CL_RConPrintPacket(from);
		return;
	}

	Com_DPrintf(S_COLOR_YELLOW "Bad server out-of-band packet from %s:\n" S_COLOR_YELLOW "%s\n", NET_AddressToString(from), string);
}

/*
 ==================
 CL_ParsePacket
 ==================
*/
static void CL_ParsePacket (const netAdr_t from, msg_t *msg){

	// Check for a connectionless packet
	if (*(int *)msg->data == OOB_SEQUENCE){
		CL_ConnectionlessPacket(from, msg);
		return;
	}

	// Dump it if not connected
	if (cls.state < CS_CONNECTED)
		return;

	if (msg->writeBytes < SERVER_PACKET_HEADER){
		Com_DPrintf(S_COLOR_YELLOW "Runt server packet from %s\n", NET_AddressToString(from));
		return;
	}

	if (!NET_CompareAddress(from, clc.netChan.remoteAddress)){
		Com_DPrintf(S_COLOR_YELLOW "Sequenced server packet without connection from %s\n", NET_AddressToString(from));
		return;
	}

	// Packet from server
	if (!NetChan_Process(&clc.netChan, cls.frameTime, msg))
		return;		// Out of order, duplicated, etc

	// Don't time-out
	clc.lastReceivedTime = cls.frameTime;

	// Parse the message
	CL_ParseServerMessage(msg);
}

/*
 ==================
 CL_ReadPackets
 ==================
*/
void CL_ReadPackets (bool dump){

	byte		data[MAX_MSG_SIZE];
	int			size;
	netAdr_t	from;
	msg_t		msg;

	// Initialize the message buffer
	MSG_Init(&msg, data, sizeof(data), false);

	// Read incoming network packets
	while (NET_GetPacket(NS_CLIENT, &from, data, &size, sizeof(data))){
		MSG_SetWriteState(&msg, size << 3);

		if (dump){
			Com_DPrintf(S_COLOR_YELLOW "Dumping server packet from %s\n", NET_AddressToString(from));
			continue;
		}

		// Parse the packet
		CL_ParsePacket(from, &msg);
	}

	// Read incoming loopback packets
	while (NET_GetLoopPacket(NS_CLIENT, &from, data, &size, sizeof(data))){
		MSG_SetWriteState(&msg, size << 3);

		if (dump){
			Com_DPrintf(S_COLOR_YELLOW "Dumping server packet from %s\n", NET_AddressToString(from));
			continue;
		}

		// Parse the packet
		CL_ParsePacket(from, &msg);
	}
}